/**
 * Repairs missing pb_Object and pb_Entity references.  It is based
 * on this article by Unity Gems: http://unitygems.com/lateral1/
 */

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using ProBuilder2.Common;
using ProBuilder2.MeshOperations;

namespace ProBuilder2.EditorCommon
{
    /**
     * Extends MonoBehaviour Inspector, automatically fixing missing script
     * references (typically caused by ProBuilder upgrade process).
     */
    [CustomEditor(typeof(MonoBehaviour))]
    public class pb_MissingScriptEditor : Editor
    {
        #region Members

        static bool applyDummyScript = true;    ///< If true, any null components that can't be set will have this script applied to their reference, allowing us to later remove them.
        static float index = 0;                 ///< general idea of where we are in terms of processing this scene.
        static float total;                     ///< general idea of how many missing script references are in this scene.

        static bool doFix = false;              ///< while true, the inspector will attempt to cycle to broken gameobjects until none are found.
        static List<GameObject> unfixable = new List<GameObject>(); ///< if a non-pb missing reference is encountered, need to let the iterator know not to bother,

        static MonoScript _mono_pb;             ///< MonoScript assets
        static MonoScript _mono_pe;             ///< MonoScript assets
        static MonoScript _mono_dummy;          ///< MonoScript assets

        /**
         * Load the pb_Object and pb_Entity classes to MonoScript assets.  Saves us from having to fall back on Reflection.
         */
        static void LoadMonoScript()
        {
            GameObject go = new GameObject();

            pb_Object pb = go.AddComponent<pb_Object>();
            pb_Entity pe = go.GetComponent<pb_Entity>();
            if (pe == null)
                pe = go.AddComponent<pb_Entity>();
            pb_DummyScript du = go.AddComponent<pb_DummyScript>();

            _mono_pb = MonoScript.FromMonoBehaviour(pb);
            _mono_pe = MonoScript.FromMonoBehaviour(pe);
            _mono_dummy = MonoScript.FromMonoBehaviour(du);

            DestroyImmediate(go);
        }

        public MonoScript pb_monoscript
        {
            get
            {
                if (_mono_pb == null) LoadMonoScript();
                return _mono_pb;
            }
        }

        public MonoScript pe_monoscript
        {
            get
            {
                if (_mono_pe == null) LoadMonoScript();
                return _mono_pe;
            }
        }

        public MonoScript dummy_monoscript
        {
            get
            {
                if (_mono_dummy == null) LoadMonoScript();
                return _mono_dummy;
            }
        }
        #endregion

        [MenuItem("Tools/" + pb_Constant.PRODUCT_NAME + "/Repair/Repair Missing Script References")]
        public static void MenuRepairMissingScriptReferences()
        {
            FixAllScriptReferencesInScene();
        }

        static void FixAllScriptReferencesInScene()
        {
            EditorApplication.ExecuteMenuItem("Window/Inspector");

            Object[] all = Resources.FindObjectsOfTypeAll(typeof(GameObject)).Where(x => ((GameObject)x).GetComponents<Component>().Any(n => n == null)).ToArray();
            total = all.Length;

            unfixable.Clear();

            if (total > 1)
            {
                Undo.RecordObjects(all, "Fix missing script references");

                index = 0;
                doFix = true;

                Next();
            }
            else
            {
                if (applyDummyScript)
                    DeleteDummyScripts();

                EditorUtility.DisplayDialog("Success", "No missing ProBuilder script references found.", "Okay");
            }
        }

        /**
         * Advance to the next gameobject with missing components.  If none are found, display dialog and exit.
         */
        static void Next()
        {
            bool earlyExit = false;

            if (EditorUtility.DisplayCancelableProgressBar("Repair ProBuilder Script References", "Fixing " + (int)Mathf.Floor(index + 1) + " out of " + total + " objects in scene.", ((float)index / total)))
            {
                earlyExit = true;
                doFix = false;
            }

            if (!earlyExit)
            {
                // Cycle through FindObjectsOfType on every Next() because using a static list didn't work for some reason.
                foreach (GameObject go in Resources.FindObjectsOfTypeAll(typeof(GameObject)))
                {
                    if (go.GetComponents<Component>().Any(x => x == null) && !unfixable.Contains(go))
                    {
                        if ((PrefabUtility.GetPrefabType(go) == PrefabType.PrefabInstance ||
                             PrefabUtility.GetPrefabType(go) == PrefabType.Prefab))
                        {
                            GameObject pref = (GameObject)PrefabUtility.GetPrefabParent(go);

                            if (pref && (pref.GetComponent<pb_Object>() || pref.GetComponent<pb_Entity>()))
                            {
                                unfixable.Add(go);
                                continue;
                            }
                        }

                        if (go.hideFlags != HideFlags.None)
                        {
                            unfixable.Add(go);
                            continue;
                        }

                        Selection.activeObject = go;

                        return;
                    }
                }
            }

            pb_Object[] pbs = (pb_Object[])Resources.FindObjectsOfTypeAll(typeof(pb_Object));

            for (int i = 0; i < pbs.Length; i++)
            {
                EditorUtility.DisplayProgressBar("Checking ProBuilder Meshes", "Refresh " + (i + 1) + " out of " + total + " objects in scene.", ((float)i / pbs.Length));

                try
                {
                    pbs[i].ToMesh();
                    pbs[i].Refresh();
                    pbs[i].Optimize();
                }
                catch (System.Exception e)
                {
                    Debug.LogWarning("Failed reconstituting " + pbs[i].name + ".  Proceeding with upgrade anyways.  Usually this means a prefab is already fixed, and just needs to be instantiated to take effect.\n" + e.ToString());
                }
            }

            EditorUtility.ClearProgressBar();

            if (applyDummyScript)
                DeleteDummyScripts();

            EditorUtility.DisplayDialog("Success", "Successfully repaired " + total + " ProBuilder objects.", "Okay");

            if (!pb_EditorSceneUtility.SaveCurrentSceneIfUserWantsTo())
                Debug.LogWarning("Repaired script references will be lost on exit if this scene is not saved!");

            doFix = false;
            skipEvent = true;
        }

        /**
         * SerializedProperty names found in pb_Entity.
         */
        List<string> PB_OBJECT_SCRIPT_PROPERTIES = new List<string>()
        {
            "_sharedIndices",
            "_vertices",
            "_uv",
            "_sharedIndicesUV",
            "_quads"
        };

        /**
         * SerializedProperty names found in pb_Object.
         */
        List<string> PB_ENTITY_SCRIPT_PROPERTIES = new List<string>()
        {
            "pb",
            "userSetDimensions",
            "_entityType",
            "forceConvex"
        };

        // Prevents ArgumentException after displaying 'Done' dialog.  For some reason the Event loop skips layout phase after DisplayDialog.
        private static bool skipEvent = false;

        public override void OnInspectorGUI()
        {
            if (skipEvent && Event.current.type == EventType.Repaint)
            {
                skipEvent = false;
                return;
            }

            SerializedProperty scriptProperty = this.serializedObject.FindProperty("m_Script");

            if (scriptProperty == null || scriptProperty.objectReferenceValue != null)
            {
                if (doFix)
                {
                    if (Event.current.type == EventType.Repaint)
                    {
                        Next();
                    }
                }
                else
                {
                    base.OnInspectorGUI();
                }

                return;
            }

            int pbObjectMatches = 0, pbEntityMatches = 0;

            // Shows a detailed tree view of all the properties in this serializedobject.
            // GUILayout.Label( SerializedObjectToString(this.serializedObject) );

            SerializedProperty iterator = this.serializedObject.GetIterator();

            iterator.Next(true);

            while (iterator.Next(true))
            {
                if (PB_OBJECT_SCRIPT_PROPERTIES.Contains(iterator.name))
                    pbObjectMatches++;

                if (PB_ENTITY_SCRIPT_PROPERTIES.Contains(iterator.name))
                    pbEntityMatches++;
            }

            // If we can fix it, show the help box, otherwise just default inspector it up.
            if (pbObjectMatches >= 3 || pbEntityMatches >= 3)
            {
                EditorGUILayout.HelpBox("Missing Script Reference\n\nProBuilder can automatically fix this missing reference.  To fix all references in the scene, click \"Fix All in Scene\".  To fix just this one, click \"Reconnect\".", MessageType.Warning);
            }
            else
            {
                if (doFix)
                {
                    if (applyDummyScript)
                    {
                        index += .5f;
                        scriptProperty.objectReferenceValue = dummy_monoscript;
                        scriptProperty.serializedObject.ApplyModifiedProperties();
                        scriptProperty = this.serializedObject.FindProperty("m_Script");
                        scriptProperty.serializedObject.Update();
                    }
                    else
                    {
                        unfixable.Add(((Component)target).gameObject);
                    }

                    Next();
                    GUIUtility.ExitGUI();
                    return;
                }
                else
                {
                    base.OnInspectorGUI();
                }

                return;
            }

            GUI.backgroundColor = Color.green;

            if (!doFix)
            {
                if (GUILayout.Button("Fix All in Scene"))
                {
                    FixAllScriptReferencesInScene();
                    return;
                }
            }

            GUI.backgroundColor = Color.cyan;

            if ((doFix && Event.current.type == EventType.Repaint) || GUILayout.Button("Reconnect"))
            {
                if (pbObjectMatches >= 3)    // only increment for pb_Object otherwise the progress bar will fill 2x faster than it should
                {
                    index++;
                }
                else
                {
                    // Make sure that pb_Object is fixed first if we're automatically cycling objects.
                    if (doFix && ((Component)target).gameObject.GetComponent<pb_Object>() == null)
                        return;
                }

                if (!doFix)
                {
                    Undo.RegisterCompleteObjectUndo(target, "Fix missing reference.");
                }

                // Debug.Log("Fix: " + (pbObjectMatches > 2 ? "pb_Object" : "pb_Entity") + "  " + ((Component)target).gameObject.name);

                scriptProperty.objectReferenceValue = pbObjectMatches >= 3 ? pb_monoscript : pe_monoscript;
                scriptProperty.serializedObject.ApplyModifiedProperties();
                scriptProperty = this.serializedObject.FindProperty("m_Script");
                scriptProperty.serializedObject.Update();

                if (doFix)
                    Next();

                GUIUtility.ExitGUI();
            }

            GUI.backgroundColor = Color.white;
        }

        /**
         * Scan the scene for gameObjects referencing `pb_DummyScript` and delete them.
         */
        static void DeleteDummyScripts()
        {
            pb_DummyScript[] dummies = (pb_DummyScript[])Resources.FindObjectsOfTypeAll(typeof(pb_DummyScript));
            dummies = dummies.Where(x => x.hideFlags == HideFlags.None).ToArray();

            if (dummies.Length > 0)
            {
                int ret = EditorUtility.DisplayDialogComplex("Found Unrepairable Objects", "Repair script found " + dummies.Length + " missing components that could not be repaired.  Would you like to delete those components now, or attempt to rebuild (ProBuilderize) them?", "Delete", "Cancel", "ProBuilderize");

                switch (ret)
                {
                    case 1: // cancel
                    {}
                     break;

                    default:
                    {
                        // Delete and ProBuilderize
                        if (ret == 2)
                        {
                            // Only interested in objects that have 2 null components (pb_Object and pb_Entity)
                            Object[] broken = (Object[])Resources.FindObjectsOfTypeAll(typeof(GameObject))
                                .Where(x => !x.Equals(null) &&
                                    x is GameObject &&
                                    ((GameObject)x).GetComponents<pb_DummyScript>().Length == 2 &&
                                    ((GameObject)x).GetComponent<MeshRenderer>() != null &&
                                    ((GameObject)x).GetComponent<MeshFilter>() != null &&
                                    ((GameObject)x).GetComponent<MeshFilter>().sharedMesh != null
                                    ).ToArray();

                            broken = broken.Distinct().ToArray();

                            ProBuilder2.Actions.ProBuilderize.DoProBuilderize(
                                System.Array.ConvertAll(broken, x => (GameObject)x)
                                .Select(x => x.GetComponent<MeshFilter>()),
                                pb_MeshImporter.Settings.Default);
                        }

                        // Always delete components
                        Undo.RecordObjects(dummies.Select(x => x.gameObject).ToArray(), "Delete Broken Scripts");

                        for (int i = 0; i < dummies.Length; i++)
                            GameObject.DestroyImmediate(dummies[i]);
                    }
                    break;
                }
            }
        }

        /**
         * Returns a formatted string with all properties in serialized object.
         */
        static string SerializedObjectToString(SerializedObject serializedObject)
        {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();

            if (serializedObject == null)
            {
                sb.Append("NULL");
                return sb.ToString();
            }

            SerializedProperty iterator = serializedObject.GetIterator();

            iterator.Next(true);


            while (iterator.Next(true))
            {
                string tabs = "";
                for (int i = 0; i < iterator.depth; i++) tabs += "\t";

                sb.AppendLine(tabs + iterator.name + (iterator.propertyType == SerializedPropertyType.ObjectReference && iterator.type.Contains("Component") && iterator.objectReferenceValue == null ? " -> NULL" : ""));

                tabs += "  - ";

                sb.AppendLine(tabs + "Type: (" + iterator.type + " / " + iterator.propertyType + " / " + " / " + iterator.name + ")");
                sb.AppendLine(tabs + iterator.propertyPath);
                sb.AppendLine(tabs + "Value: " + SerializedPropertyValue(iterator));
            }

            return sb.ToString();
        }

        /**
         * Return a string from the value of a SerializedProperty.
         */
        static string SerializedPropertyValue(SerializedProperty sp)
        {
            switch (sp.propertyType)
            {
                case SerializedPropertyType.Integer:
                    return sp.intValue.ToString();

                case SerializedPropertyType.Boolean:
                    return sp.boolValue.ToString();

                case SerializedPropertyType.Float:
                    return sp.floatValue.ToString();

                case SerializedPropertyType.String:
                    return sp.stringValue.ToString();

                case SerializedPropertyType.Color:
                    return sp.colorValue.ToString();

                case SerializedPropertyType.ObjectReference:
                    return (sp.objectReferenceValue == null ? "null" : sp.objectReferenceValue.name);

                case SerializedPropertyType.LayerMask:
                    return sp.intValue.ToString();

                case SerializedPropertyType.Enum:
                    return sp.enumValueIndex.ToString();

                case SerializedPropertyType.Vector2:
                    return sp.vector2Value.ToString();

                case SerializedPropertyType.Vector3:
                    return sp.vector3Value.ToString();

                // Not public api as of 4.3?
                // case SerializedPropertyType.Vector4:
                //      return sp.vector4Value.ToString();

                case SerializedPropertyType.Rect:
                    return sp.rectValue.ToString();

                case SerializedPropertyType.ArraySize:
                    return sp.intValue.ToString();

                case SerializedPropertyType.Character:
                    return "Character";

                case SerializedPropertyType.AnimationCurve:
                    return sp.animationCurveValue.ToString();

                case SerializedPropertyType.Bounds:
                    return sp.boundsValue.ToString();

                case SerializedPropertyType.Gradient:
                    return "Gradient";

                default:
                    return "Unknown type";
            }
        }
    }
}
